Repository navigation
Workouts: backfill HR/calories on a manual entry from its detected bout (#510) - #708
Merged
ryanbr merged 1 commit intoJul 22, 2026
Merged
Conversation
…ut (ryanbr#510) A manually-entered (retroactive) workout can end up with no Avg HR / calories even on a single-strap install with dense HR coverage: the auto-workout detector finds the same activity from raw HR+motion, computes a real avgHR/calories/maxHR/strain for it, then discards all of that the moment it overlaps a real (manual/imported) row - purely to avoid double-counting the same workout twice. A manual entry's user-typed [startTs,endTs] rarely matches the detector's own tighter boundaries, so the display-time strap-HR fill (WhoopRepository.fillWorkoutHrFromStrap / raw window read) can come up too thin (<60 samples) inside the row's own window even though the detector, working from its own boundaries, found plenty. Fix: when a detected bout collides with a real row, backfill only the fields that row doesn't already have (never touching anything typed by the user, imported, or already filled) from the bout's own computed values, then upsert in place (same natural key). Extracted the merge into a small pure, tested helper on each platform (WorkoutDetector.backfillWorkout / IntelligenceEngine.backfillWorkoutFromDetectedBout). New trace verdict "droppedOverlapBackfilled" distinguishes a backfilled drop from a plain one in the Workouts test-mode trace. Root cause confirmed against the reporter's own Test Centre workouts trace: `detectedBout verdict=droppedOverlap durMin=11 avgBpm=150 overlapSource=manual` - a real avgBpm was computed and thrown away.
digitalerdude
marked this pull request as ready for review
July 21, 2026 15:45
DX23876
pushed a commit
to DX23876/noop
that referenced
this pull request
Jul 22, 2026
…alories-manual Workouts: backfill HR/calories on a manual entry from its detected bout (ryanbr#510)
simoncad7
pushed a commit
to simoncad7/noop
that referenced
this pull request
Aug 17, 2026
…alories-manual Workouts: backfill HR/calories on a manual entry from its detected bout (ryanbr#510)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Addresses the confirmed, narrowed part of #510: "the issue with the missing HR and calories in the workouts is still there. However, the values are only missing when I manually enter the workout afterward. When I start and end a workout through the app, the values are displayed correctly." (The Effort-score part of the original report turned out to be unrelated — wearing the strap on the bicep for a more accurate HR reading, per the reporter's own follow-up.)
Root cause
Confirmed directly against the reporter's own Test Centre → Workouts trace (attached to the issue):
IntelligenceEngine's auto-workout detector independently finds the same activity from raw HR + motion and computes a realavgHR/calories/maxHR/strain for it (avgBpm=150right there in the trace) — then throws all of that away the instant it overlaps a real (manual/imported) row, purely to avoid persisting the same workout twice (#975).Separately,
WhoopRepository.fillWorkoutHrFromStrap(the display-time fill) reads HR strictly inside the row's own[startTs, endTs]window. A manual entry's user-typed boundaries rarely match the detector's own (tighter) boundaries for the same activity, so that raw-window read can come up underminSamples(60) even on a single-strap install with dense HR coverage — while the detector, using its own boundaries, found plenty. Net effect: real, already-computed HR/calories data existed for the exact activity and was silently discarded instead of being used.Fix
When a detected bout collides with a real row, backfill only the fields that row doesn't already have — never touching anything typed by the user, imported, or previously filled — from the bout's own computed values, then upsert in place (same natural key
(deviceId, startTs, sport), so this updates rather than duplicates).Extracted the merge into a small, pure, tested helper on each platform so the logic is unit-testable without standing up the whole
analyzeRecentpipeline:IntelligenceEngine.backfillWorkoutFromDetectedBoutWorkoutDetector.backfillWorkout(inPackages/StrandAnalytics, so it's covered byswift testrather than only the App-target test suite)Added a new trace verdict
droppedOverlapBackfilled(alongside the existingpersisted/droppedOverlap) so the Workouts test-mode trace can distinguish "dropped and backfilled" from "dropped, nothing to add" — useful for the next report like this one.Scope
Byte-parity: both platforms touched in this PR, same natural key / same field semantics, no analytics formula or stored-value shape changed — this only decides which of the fields a row already has get filled in from data the pipeline already computes.
Testing
IntelligenceEngineWorkoutBackfillTest(3 tests)WorkoutDetectorBackfillTests(3 tests)./gradlew compileFullDebugKotlin testFullDebugUnitTest --tests "com.noop.analytics.*"✓ (JDK 17 via Homebrew)swift test(Packages/StrandAnalytics): 1110 tests, 0 failures ✓xcodegen generate && xcodebuild -project Strand.xcodeproj -scheme Strand -destination 'platform=macOS' CODE_SIGNING_ALLOWED=NO build— BUILD SUCCEEDEDxcodebuild -project Strand.xcodeproj -scheme NOOPiOS -destination 'generic/platform=iOS Simulator' CODE_SIGNING_ALLOWED=NO build— BUILD SUCCEEDED